package com.xrui.hbaseview.converters;

import com.xrui.hbaseview.config.GlobalConfig;
import com.xrui.hbaseview.io.PathHelper;
import com.xrui.hbaseview.reflection.JavaPackage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.lang.reflect.Modifier;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.*;

/**
 * This class is responsible for loading and creation of type converters.
 */
@SuppressWarnings({"CallToPrintStackTrace", "ResultOfMethodCallIgnored"})
public class ConvertersLoader {

    //region Variables
    private final static Logger LOG = LoggerFactory.getLogger(ConvertersLoader.class);
    private static final List<ConvertersLoaderHandler> handlers;
    private static Map<String, TypeConverter> converters;
    //endregion

    //region Constructor
    static {
        handlers = new ArrayList<>();
        converters = load();
    }

    private ConvertersLoader() {
    }
    //endregion

    //region Public Methods

    /**
     * Adds a handler.
     *
     * @param handler The new handler.
     */
    public static void addHandler(ConvertersLoaderHandler handler) {
        handlers.add(handler);
    }

    /**
     * Removes a handler.
     *
     * @param handler A handler to remove.
     */
    public static void removeHandler(ConvertersLoaderHandler handler) {
        handlers.remove(handler);
    }

    /**
     * Checks whether the specified converter exists.
     *
     * @param name The name of the converter to check.
     * @return True if the specified converter exists or False otherwise.
     */
    public static boolean exists(String name) {
        return converters.containsKey(name);
    }

    /**
     * Gets all loaded converters.
     *
     * @return A list of type converters.
     */
    public static Collection<TypeConverter> getConverters() {
        return converters.values();
    }

    /**
     * Gets all loaded converters that support column name conversions.
     *
     * @return A list of type converters.
     */
    public static Collection<TypeConverter> getNameConverters() {
        Collection<TypeConverter> list = new ArrayList<TypeConverter>();
        for (TypeConverter converter : converters.values()) {
            if (converter.isValidForNameConversion()) {
                list.add(converter);
            }
        }
        return list;
    }

    /**
     * Gets a specific converter according to the provided name.
     *
     * @param name The name of the converter to get.
     * @return A type converter if found or null otherwise.
     */
    public static TypeConverter getConverter(String name) {
        return converters.get(name);
    }

    /**
     * Handles converter editing.
     *
     * @param oldName A new converter name if the name was changed or the old one.
     * @param newName A new converter name if the name was changed or the old one.
     */
    public static void editConverter(String oldName, String newName) {
        if (!oldName.equals(newName)) {
            deleteConverter(oldName);
        }

        reload();

        for (ConvertersLoaderHandler handler : handlers) {
            handler.onEdit(oldName, newName);
        }
    }

    /**
     * Removes converter from the loader's cache and from the file system.
     *
     * @param name The name of the converter to remove.
     */
    public static void removeConverter(String name) {
        if (deleteConverter(name)) {
            reload();

            for (ConvertersLoaderHandler handler : handlers) {
                handler.onRemove(name);
            }
        }
    }

    /**
     * Reloads converters.
     */
    public static void reload() {
        converters = load();

        for (ConvertersLoaderHandler handler : handlers) {
            handler.onLoad();
        }
    }
    //endregion

    //region Private Methods
    private static boolean deleteConverter(String name) {
        TypeConverter converter = converters.get(name);
        if (converter != null) {
            converters.remove(name);

            File classFile = new File(
                PathHelper.append(
                    PathHelper.append(GlobalConfig.instance().getConvertersClassesFolder(), "com/tr/ts/hbase/tools/hviewer/converters/custom"),
                    converter.getClass().getSimpleName() + ".class"));

            if (classFile.exists()) {
                classFile.delete();
            }

            File codeFile = new File(PathHelper.append(GlobalConfig.instance().getConvertersCodeFolder(), converter.getClass().getSimpleName() + ".java"));
            if (codeFile.exists()) {
                codeFile.delete();
            }

            return true;
        }
        return false;
    }

    private static Map<String, TypeConverter> load() {
        Map<String, TypeConverter> map = new TreeMap<>();

        loadPackage("com.xrui.hbaseview.converters", map);
        loadFolder(GlobalConfig.instance().getConvertersClassesFolder(), "com.tr.ts.hbase.tools.hviewer.converters.custom", map);

        return map;
    }

    private static void loadPackage(String packageName, Map<String, TypeConverter> map) {
        try {
            for (Class<?> clazz : JavaPackage.getClasses(packageName)) {
                if (TypeConverter.class.isAssignableFrom(clazz) && !Modifier.isAbstract(clazz.getModifiers())) {
                    try {
                        TypeConverter converter = (TypeConverter) clazz.getConstructor().newInstance();
                        map.put(converter.getName(), converter);
                    } catch (Exception e) {
                        LOG.error(String.format("Failed to load converter '%s'", clazz.getName()), e);
                    }
                }
            }
        } catch (Exception e) {
            LOG.error(String.format("Failed to load converters from the package '%s'", packageName), e);
        }
    }

    private static void loadFolder(String folder, String packageName, Map<String, TypeConverter> map) {
        try {
            URLClassLoader loader = new URLClassLoader(
                new URL[]{
                    new File(folder).toURI().toURL()
                }, Thread.currentThread().getContextClassLoader());

            for (Class<?> clazz : JavaPackage.getClassesFromFolder(loader, new File(folder), packageName)) {
                if (TypeConverter.class.isAssignableFrom(clazz) && !Modifier.isAbstract(clazz.getModifiers())) {
                    try {
                        TypeConverter converter = (TypeConverter) clazz.getConstructor().newInstance();
                        converter.setCode(
                            loadCode(
                                new File(
                                    PathHelper.append(
                                        GlobalConfig.instance().getConvertersCodeFolder(),
                                        converter.getClass().getSimpleName() + ".java")
                                )
                            )
                        );

                        map.put(converter.getName(), converter);
                    } catch (Exception e) {
                        LOG.error(String.format("Failed to load converter '%s'", clazz.getName()), e);
                    }
                }
            }
        } catch (Exception e) {
            LOG.error(String.format("Failed to load converters from the folder '%s' for the package '%s'", folder, packageName), e);
        }
    }

    private static String loadCode(File file) throws IOException {
        if (file.exists()) {
            StringBuilder code = new StringBuilder();

            BufferedReader reader = null;
            try {
                reader = new BufferedReader(new FileReader(file));

                String line;
                while ((line = reader.readLine()) != null) {
                    code.append(line);
                    code.append(PathHelper.LINE_SEPARATOR);
                }
                return code.toString();
            } finally {
                if (reader != null) {
                    reader.close();
                }
            }
        }
        return null;
    }
    //endregion
}
